/**
 * request，该工具包依赖 net/http 封装了 HTTP 的请求方法，便捷化、规范化 HTTP 请求操作
 * 创建者：Bryce
 * 创建时间：20210724
 */
package unit

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"github.com/pkg/errors"
	"github.com/pquerna/ffjson/ffjson"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"strings"
	"time"
)

type HTTPMethod string

const (
	GET     HTTPMethod = "GET"
	POST    HTTPMethod = "POST"
	PUT     HTTPMethod = "PUT"
	PATCH   HTTPMethod = "PATCH"
	DELETE  HTTPMethod = "DELETE"
	OPTION  HTTPMethod = "OPTION"
	HEAD    HTTPMethod = "HEAD"
	TRACE   HTTPMethod = "TRACE"
	CONNECT HTTPMethod = "CONNECT"
)

// HttpRequest HTTP 请求条件结构
type HttpRequest struct {
	Method        HTTPMethod        // 请求方式，范围：GET/POST/PUT/PATCH/DELETE/OPTIONS/HEAD/TRACE/CONNECT
	Url           string            // 请求地址
	Args          interface{}       // 请求参数结构体的地址指针，参数为空则传入 nil
	FormData      map[string]string // 表单类型参数
	BanHtmlEscape bool              // 是否禁止参数字符串自动进行 HTMLEscape 转码，false-默认自动转码/true-不转码
	Header        map[string]string // 请求头信息，不存在传入nil，存在传入map[string]string格式头信息数据
	Ttl           int               // 请求超时时间，单位秒，默认 60 秒
	Retry         int               // HTTP 请求失败重试次数
	Sleep         int               // HTTP 请求失败重试间隔时间，单位秒，默认 3 秒
	Reply         interface{}       // http 请求接收响应结构体指针
	File          io.Reader         // 待上传的文件 IO 读取接口
}

type HttpResponse struct {
	Header http.Header // http 响应头
	Body   []byte      // http 响应内容的字节切片
	Code   int         // http 请求响应码
}

// Request HTTP 请求执行方法
func (h *HttpRequest) Request() (res *HttpResponse, err error) {
	// 1. 组装 http 请求客户端
	t := 60
	if h.Ttl != 0 {
		t = h.Ttl
	}
	client := &http.Client{
		// 设置客户端连接超时、读取请求体超时
		Timeout: time.Duration(t) * time.Second,
		// 设备客户端连接跳过证书检查
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		},
	}
	// 2.执行请求
	res = &HttpResponse{Code: 0}
	response, err := h.request(client)
	if err != nil {
		return res, err
	}
	if response == nil {
		return res, errors.New("response is nil")
	}
	defer response.Body.Close()
	// 3. 获取响应结果
	res.Code = response.StatusCode
	res.Header = response.Header
	if response.Body == nil {
		return res, errors.New("response body is nil")
	}
	// 4. 获取响应内容
	buf, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return res, err
	}
	res.Body = buf
	// 5. 判断是否传入结果结构体指针，传入则进行赋值操作
	if h.Reply != nil && len(buf) > 0 && response.StatusCode == 200 {
		err = ffjson.Unmarshal(buf, h.Reply)
	}
	return res, err
}

// HTTP 通讯请求，添加误重试机制
func (h *HttpRequest) request(client *http.Client) (res *http.Response, err error) {
	// 设置默认值
	if h.Sleep == 0 {
		h.Sleep = 3
	}
	if h.Retry == 0 {
		h.Retry = 1
	}
	// 获取请求参数
	var args io.Reader
	if h.FormData != nil {
		args, err = h.getFormData()
	} else {
		args, err = h.argsToString()
	}
	if err != nil {
		return nil, err
	}
	// 执行请求
	for i := 1; i <= h.Retry; i++ {
		// 1.建立请求
		request, err := http.NewRequest(string(h.Method), h.Url, args)
		if err != nil {
			return nil, errors.WithStack(err)
		}
		request.Close = true
		// 2.设置请求头
		if len(h.Header) != 0 {
			for k, v := range h.Header {
				request.Header.Set(k, v)
			}
		}
		// 3.执行请求
		res, err = client.Do(request)
		if err == nil {
			return res, errors.WithStack(err)
		}
		// 4.判断是否还需要重试
		if i < h.Retry {
			if res != nil && res.Body != nil {
				res.Body.Close()
			}
			time.Sleep(time.Duration(h.Sleep) * time.Second)
		} else {
			return res, errors.WithStack(err)
		}
	}
	return
}

func (h *HttpRequest) getFormData() (args io.Reader, err error) {
	// 组装参数
	body := new(bytes.Buffer)
	w := multipart.NewWriter(body)
	for k, v := range h.FormData {
		if err = w.WriteField(k, v); err != nil {
			return nil, errors.WithStack(err)
		}
	}
	w.Close()
	// 2. 设置请求头
	if h.Header != nil {
		h.Header["Content-Type"] = w.FormDataContentType()
	} else {
		h.Header = map[string]string{"Content-Type": w.FormDataContentType()}
	}
	return body, nil
}

// HTTP 请求参数转字符串
func (h *HttpRequest) argsToString() (args io.Reader, err error) {
	// 验证请求参数是否存在
	if h.Args == nil {
		return nil, nil
	}
	// 验证参数是否为纯字符串
	if v, ok := h.Args.(string); ok {
		return strings.NewReader(v), nil
	}
	// 验证是否禁止
	if !h.BanHtmlEscape {
		buf, err := ffjson.Marshal(h.Args)
		return strings.NewReader(string(buf)), errors.WithStack(err)
	}
	// 禁止 HTML 特殊字符转义编码
	buf := bytes.NewBuffer([]byte{})
	jsonEncoder := json.NewEncoder(buf)
	jsonEncoder.SetEscapeHTML(false)
	err = jsonEncoder.Encode(h.Args)
	return strings.NewReader(buf.String()), errors.WithStack(err)
}

// Upload HTTP 上传文件
func (h *HttpRequest) Upload() (int, error) {
	// 1. 组装 http 请求客户端
	t := 1800
	if h.Ttl != 0 {
		t = h.Ttl
	}
	client := &http.Client{
		// 设置客户端连接超时、读取请求体超时
		Timeout: time.Duration(t) * time.Second,
		// 设备客户端连接跳过证书检查
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		},
	}
	// 2. 组装请求
	request, err := http.NewRequest(string(h.Method), h.Url, h.File)
	if err != nil {
		return 0, errors.WithStack(err)
	}
	// 3. 组装请求头
	if len(h.Header) != 0 {
		for k, v := range h.Header {
			request.Header.Set(k, v)
		}
	}
	// 4. 执行请求
	res, err := client.Do(request)
	if err != nil {
		return 0, errors.WithStack(err)
	}
	defer res.Body.Close()

	return res.StatusCode, nil
}
